Skip to content

feat: term.update() folds CapabilityEvents into runtime capabilities - #133

Merged
natemoo-re merged 10 commits into
mainfrom
feat/term-capability-sink
Oct 9, 2026
Merged

natemoo-re merged 10 commits into
mainfrom
feat/term-capability-sink

Conversation

@natemoo-re

@natemoo-re natemoo-re commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Part 4/4 (final) of the terminfo foundation stack. Requires #132.

  • term.update() is now the single entry point for resize and capability changes: pass an array of InputEvents (resize is { type: "resize", width, height }); it returns bytes to write (empty for now)
  • createTerm({ terminfo }) seeds the renderer from a TerminalInfo; term.capabilities exposes the current snapshot (256-color baseline when omitted)
  • validated() forwards the new capabilities getter
  • Changeset covers detectTerminal/TerminalInfo, the createTerm option, and the update({ events }) / update({ width, height }) → update(events) break
  • Out of scope: no change to rendered bytes yet; first consumer is color encoding modes (💡 Color-encoding modes (16-color / 256-color) derived from terminfo #60), other protocol follow-ups in Track terminfo capability follow-ups #129

Split from #106 for reviewability; #106 closes when this stack lands.

@natemoo-re
natemoo-re added this pull request to stack #134 September 19, 2026 04:29
@bombshell-cooper

bombshell-cooper Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Changeset present

This pull request is classified as breaking_change: @bomb.sh/tty

Comment thread .changeset/terminfo-capability-layer.md
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Size Increased — +10.1 KB

122.6 KB unpacked

@pkg-pr-new

pkg-pr-new Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@bomb.sh/tty@133

commit: 33c2ceb

@natemoo-re natemoo-re changed the title feat: term.update() folds CapabilityEvents into runtime capabilities (renderer-spec §7.7, §8.6) feat: term.update() folds CapabilityEvents into runtime capabilities Sep 19, 2026
@codspeed

codspeed Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 10 untouched benchmarks


Comparing feat/term-capability-sink (33c2ceb) with main (d8f2a24)

Open in CodSpeed

@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from c64972d to d5be56e Compare September 30, 2026 01:50
natemoo-re added a commit that referenced this pull request Sep 30, 2026
Widen Update from { width, height } | CapabilityEvent to
{ width, height } | InputEvent. Events other than resize and capability
are no-op steps, so hosts can pass the full scan() events array without
filtering. Only ResizeEvent carries width/height, so the shapes stay
unambiguous. Matches the implementation in #133.
@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from 42fa5a4 to f124b5d Compare September 30, 2026 02:01
@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from f124b5d to 5be02ae Compare September 30, 2026 02:03
@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from 5be02ae to 34fca15 Compare September 30, 2026 15:11
@natemoo-re
natemoo-re marked this pull request as ready for review September 30, 2026 15:12
@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch 5 times, most recently from eb3b262 to 3841243 Compare October 8, 2026 13:59
Comment thread term.ts Outdated
@cowboyd

cowboyd commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Is the eventual plan to store the authoritative (mutable) copy of the terminfo in WASM?

I don't think the spec has an opinion on this, but how do we anticipate using the capabilities snapshot when we render? If it is inside the WASM, the we should probably store it there and not copy it over every time.

@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from b3d41fb to 1999ea0 Compare October 9, 2026 02:48
@natemoo-re natemoo-re mentioned this pull request Oct 9, 2026
@natemoo-re

Copy link
Copy Markdown
Member Author

Is the eventual plan to store the authoritative (mutable) copy of the terminfo in WASM?

I don't think the spec has an opinion on this, but how do we anticipate using the capabilities snapshot when we render? If it is inside the WASM, the we should probably store it there and not copy it over every time.

right, nothing actually reads capabilities yet, but it probably should end up in WASM once #60 lands. the API doesn't change, so I'd like to defer that decision to #60—left a note in the spec so we track it.

@cowboyd cowboyd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome! sounds like a plan

@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from 1999ea0 to 50c99fc Compare October 9, 2026 15:17
Base automatically changed from feat/input-probe-events to main October 9, 2026 15:18
The terminfo spec no longer lists applyUpdate as public API; update()
is the only entry point, and tests already exercise it through
term.update().
The renderer spec now accepts any InputEvent as an Update and requires
non-resize, non-capability events to change no state and emit no bytes.
Match createInput: the renderer option is `terminfo`, typed
`TerminalInfo`. Adds the first test that seeds createTerm from a detected
TerminalInfo (previously untested; test/caps.ts helpers were unused).

The stack changeset now covers only what this PR and #131 add —
detectTerminal/TerminalInfo, createTerm's option, term.capabilities, and
the update() signature change — since #132 carries its own changeset for
the input side.
Replace the `Update | readonly Update[]` overload with a single
`update(events: readonly InputEvent[])` signature (renderer-spec §7.7,
§8.6; terminfo-spec §10.4, §10.5).

ResizeEvent is already a member of InputEvent, so a separate `Update`
union adds nothing. Discriminating on `type` instead of a structural
`{ width, height }` shape makes every step a tagged variant. The host
loop collapses to passing `scan().events` straight through, and
out-of-band resizes (SIGWINCH) pass a constructed ResizeEvent.

The bare `{ width, height }` shape shipped in 0.9.0 is removed in the
same release that already breaks `update({ events })`, so callers
migrate once.
Implements the spec change in the previous commit. The `Update` type
and the single-or-array overload are gone; update() takes
`readonly InputEvent[]` and folds each event in order.

Steps now discriminate on `type`: `"resize"` resizes, `"capability"`
folds into RuntimeCapabilities, everything else is a no-op. That drops
the structural `"width" in` check and every cast in applyUpdate, since
CapabilityEvent narrows by `key`. A new test pins the tag-based
discrimination: an untagged `{ width, height }` no longer resizes.

validated() forwards the new signature unchanged.
The migration now covers both shapes 0.9.0 shipped in #113,
`update({ events })` and `update({ width, height })`. Both become an
array of InputEvents, with resizes expressed as
`{ type: "resize", width, height }`.
Records the current design and the expected direction in §13, so the
question of where the authoritative copy lives has an answer on record.

Today RuntimeCapabilities lives only in the TypeScript Term closure:
no renderer output reads it, so nothing crosses into WASM. Nearly every
planned consumer (color encoding in #60, erase strategy from
bce/autoMargin/xenl, sync-output wrapping) lives in C. The expected
direction is to make WASM authoritative so render() reads capabilities
without per-frame transfer. The public contract (update() folds,
term.capabilities is a frozen snapshot) is the same either way, so the
storage decision is deferred to #60, where the first consumer lands.
The TermOptions.terminfo docstring said createTerm "seeds the private
TermInfo struct", but createTerm never touches a C struct: it seeds
term.capabilities in TypeScript.

The terminfo.h header described the #106 design, where the renderer
read a shared struct and the input parser wrote probe responses into it.
terminfo-spec §4.2 and TINV-6 replaced that: the two share no memory,
and the struct is only used by detectTerminal(). The TERMINFO_DA1
comment named a queryTermInfo probe window that no longer exists.

terminfo_confirm and TERMINFO_DA1 stay: if #60 makes WASM authoritative
for capabilities (renderer-spec §13), they are the natural fold path.
@natemoo-re
natemoo-re force-pushed the feat/term-capability-sink branch from 50c99fc to 3272810 Compare October 9, 2026 15:18
@natemoo-re
natemoo-re merged commit 2c6215c into main Oct 9, 2026
12 checks passed
@natemoo-re
natemoo-re deleted the feat/term-capability-sink branch October 9, 2026 15:24
natemoo-re added a commit that referenced this pull request Oct 9, 2026
… them (renderer-spec §7.9)

Elements declare open(id, { pointerShape }). While
term.capabilities.pointerShape is true (raised by #132/#133 from the
probe's OSC 22 query reply), each render resolves the shape under the
pointer and appends `OSC 22 ; <shape> ST` to result.output when it
changes. The host keeps writing one buffer (cowboyd on #101). The
capability is the only gate; there is no createTerm option.

Everything lives in TypeScript; the wasm module and the packed encoding
are untouched, so there is no size or startup cost (#101 regressed
createTerm ~20% and grew the bundle 8.3 KB). Without the capability the
render path pays one boolean check and never reads pointerShape (a test
pins this with a counting getter). With it, the directive walk runs only
when the pointer is over something, and only a frame whose shape
changed copies output to append the sequence.

Resolution takes the last declaring id in Clay's pointer-over order:
pre-order within the topmost layer the pointer reaches, so the innermost
element wins and capture-mode floats hide what is beneath while
passthrough floats defer to it. snapshot() records its declared shapes
beside the packed bytes so pre-packed subtrees still participate.
Values outside the kitty/CSS vocabulary are ignored (and rejected by
validate()), so arbitrary strings never reach an OSC payload.

Restore (dreyfus92 on #101): set-only OSC 22 cannot pop, so the emitted
shape is the one piece of cross-frame state. It resets to "default"
when the pointer leaves or is omitted, and update() returns the reset
when the pointer-shape capability is withdrawn. Resize keeps it, since
the terminal's pointer did not change.
natemoo-re added a commit that referenced this pull request Oct 10, 2026
… them (renderer-spec §7.9)

Elements declare open(id, { pointerShape }). While
term.capabilities.pointerShape is true (raised by #132/#133 from the
probe's OSC 22 query reply), each render resolves the shape under the
pointer and appends `OSC 22 ; <shape> ST` to result.output when it
changes. The host keeps writing one buffer (cowboyd on #101). The
capability is the only gate; there is no createTerm option.

Everything lives in TypeScript; the wasm module and the packed encoding
are untouched, so there is no size or startup cost (#101 regressed
createTerm ~20% and grew the bundle 8.3 KB). Without the capability the
render path pays one boolean check and never reads pointerShape (a test
pins this with a counting getter). With it, the directive walk runs only
when the pointer is over something, and only a frame whose shape
changed copies output to append the sequence.

Resolution takes the last declaring id in Clay's pointer-over order:
pre-order within the topmost layer the pointer reaches, so the innermost
element wins and capture-mode floats hide what is beneath while
passthrough floats defer to it. snapshot() records its declared shapes
beside the packed bytes so pre-packed subtrees still participate.
Values outside the kitty/CSS vocabulary are ignored (and rejected by
validate()), so arbitrary strings never reach an OSC payload.

Restore (dreyfus92 on #101): set-only OSC 22 cannot pop, so the emitted
shape is the one piece of cross-frame state. It resets to "default"
when the pointer leaves or is omitted, and update() returns the reset
when the pointer-shape capability is withdrawn. Resize keeps it, since
the terminal's pointer did not change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants